﻿using System;
using System.Text;
using Ionic.Zlib;
using System.IO;

namespace NamedBinaryTag
{
	/// <summary>
	/// Represents a Named Binary File data.
	/// </summary>
	public sealed class TagFile : System.ComponentModel.INotifyPropertyChanged
	{

		/// <summary>
		/// Obtains the file Root Tag.
		/// </summary>
		/// <remarks>
		/// When building a new file, you must call <see cref="Init"/> before accessing this member.
		/// </remarks>
		public TagCompound RootTag { get; private set; }
		/// <summary>
		/// Obtains a flag indicating if compression is used for writing.
		/// </summary>
		/// <remarks>
		/// When <see cref="Load"/> is called, the value may be updated.
		/// </remarks>
		public bool Compress { get { return Compression != CompressionType.None; } }
		/// <summary>
		/// Obtains or defines the compression to use.
		/// </summary>
		/// <remarks>
		/// When <see cref="Load"/> is called, the value is updated according to the loaded file.
		/// </remarks>
		public CompressionType Compression { get; set; }

		/// <summary>
		/// Loads <paramref name="input"/> stream as a Named Binary Tag file.
		/// </summary>
		/// <param name="input">A <see cref="System.IO.Stream"/> opened for reading.</param>
		/// <remarks>
		/// Raises property updates.
		/// </remarks>
		public void Load(Stream input)
		{
			TagCompound root = new TagCompound();
			using (System.IO.Stream headered = TagHelper.WrapHeadered(input))
			{
				CompressionType type = TagHelper.DetermineCompression(headered);
				if (!TryLoad(headered, root, type))
				{
					throw new ArgumentException("Could not successfully decode the stream.", lastException);
				}
			}
			RootTag = root;
			RaisePropertyChange("RootTag");
			RaisePropertyChange("Empty");
			RaisePropertyChange("Valid");
		}

		/// <summary>
		/// Loads <paramref name="input"/> stream as a Named Binary Tag with the specified compression.
		/// </summary>
		/// <param name="input">A <see cref="System.IO.Stream"/> opened for reading.</param>
		/// <param name="compression">The compression to read the <paramref name="input"/> with.</param>
		/// <remarks>
		/// Raises property updates.
		/// </remarks>
		/// <exception cref="ArgumentException">An error occured while trying to load the data. Details in the inner exception.</exception>
		public void Load(Stream input, CompressionType compression)
		{
			TagCompound root = new TagCompound();
			using (System.IO.Stream headered = TagHelper.WrapHeadered(input))
			{
				if (!TryLoad(headered, root, compression))
				{
					throw new ArgumentException("Could not successfully decode the stream.", lastException);
				}
			}
			RootTag = root;
			RaisePropertyChange("RootTag");
			RaisePropertyChange("Empty");
			RaisePropertyChange("Valid");
		}

#region Load Try

		/// <summary>
		/// Latest thrown exception while trying to load.
		/// </summary>
		private Exception lastException;

		private bool TryLoad(Stream input, TagCompound root, CompressionType compression)
		{
			lastException = null;
			switch (compression)
			{
				case CompressionType.None:
					return TryLoadNone(input, root);
				case CompressionType.GZip:
					return TryLoadGZip(input, root);
				case CompressionType.Zlib:
					return TryLoadZlib(input, root);
			}
			return false;
		}

		private bool TryLoadGZip(Stream input, TagCompound root)
		{
			try
			{
				using (System.IO.Stream gzstream = TagHelper.WrapGZip(input))
				{
					LoadData(root, gzstream);
				}
				Compression = CompressionType.GZip;
			}
			catch (ZlibException e)
			{
				lastException = e;
				return false;
			}
			return true;
		}

		private bool TryLoadZlib(Stream input, TagCompound root)
		{
			try
			{
				using (System.IO.Stream zlibstream = TagHelper.WrapZLib(input))
				{
					LoadData(root, zlibstream);
				}
				Compression = CompressionType.Zlib;
			}
			catch (ZlibException e)
			{
				lastException = e;
				return false;
			}
			return true;
		}

		private bool TryLoadNone(Stream input, TagCompound root)
		{
			LoadData(root, input);
			Compression = CompressionType.None;
			return true;
		}

#endregion

		/// <summary>
		/// Loads the <paramref name="stream"/> data into the provide tag.
		/// </summary>
		/// <param name="root">The tag to load data into.</param>
		/// <param name="stream">The uncompressed <see cref="System.IO.Stream"/> to read.</param>
		private static void LoadData(TagCompound root, Stream stream)
		{
			using (DumpStream dump = new DumpStream(stream))
			{
				using (BinaryReader reader = new BinaryReader(dump, Encoding.UTF8))
				{
					TagByte type = new TagByte();
					type.Read(reader, true);
					if (type.Value != root.TagType)
					{
#if !SILVERLIGHT
								throw new InvalidDataException("Root Tag_Compound not found.");
#else
						throw new Exception("Root Tag_Compound not found.");
#endif
					}
					root.Read(reader);
				}
			}
		}

		/// <summary>
		/// Saves this data file into the <paramref name="output"/> stream.
		/// </summary>
		/// <param name="output">A <see cref="System.IO.Stream"/> opened for writing</param>
		/// <exception cref="System.InvalidOperationException">This data file is not initialized.</exception>
		public void Save(Stream output)
		{
			if (RootTag == null)
				throw new InvalidOperationException("The Root Tag is not initialized.");
			switch (Compression)
			{
				case CompressionType.None:
					SaveData(output);
					break;
				case CompressionType.GZip:
					using (Stream stream = new GZipStream(output, CompressionMode.Compress, true))
					{
						SaveData(stream);
					}
					break;
				case CompressionType.Zlib:
					using (Stream stream = new ZlibStream(output, CompressionMode.Compress, true))
					{
						SaveData(stream);
					}
					break;
			}
		}

		/// <summary>
		/// Saves the data into the provided <paramref name="stream"/>.
		/// </summary>
		/// <param name="stream">The <see cref="System.IO.Stream"/> to write uncompressed data to.</param>
		private void SaveData(Stream stream)
		{
			using (BinaryWriter writer = new BinaryWriter(stream, Encoding.UTF8))
			{
				TagByte type = new TagByte();
				type.Value = RootTag.TagType;
				type.Write(writer, true);
				RootTag.Write(writer);
			}
		}

		/// <summary>
		/// Reinitialize this data file.
		/// </summary>
		public void Init()
		{
			RootTag = new TagCompound();
		}

		#region INotifyPropertyChanged Members

		public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

		#endregion

		private void RaisePropertyChange(string propertyName)
		{
			if (PropertyChanged != null)
				PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
		}

		/// <summary>
		/// Exchange the data with another file.
		/// </summary>
		/// <param name="other">The file to exchange content with.</param>
		/// <remarks>
		/// Raises property updates for both files.
		/// </remarks>
		public void Swap(TagFile other)
		{
			if ((other == null) || (other == this))
				return;
			TagCompound rootTag = RootTag;
			CompressionType compression = Compression;
			RootTag = other.RootTag;
			Compression = other.Compression;
			other.RootTag = rootTag;
			other.Compression = compression;
			RaisePropertyChange("RootTag");
			RaisePropertyChange("Empty");
			RaisePropertyChange("Valid");
			other.RaisePropertyChange("RootTag");
			other.RaisePropertyChange("Empty");
			other.RaisePropertyChange("Valid");
		}

		/// <summary>
		/// Obtains emptyness of the file.
		/// </summary>
		/// <remarks>
		/// Emptyness is defined as the abscence of value for <see cref="RootTag"/>.
		/// </remarks>
		public bool Empty
		{
			get { return (RootTag == null); }
		}

		/// <summary>
		/// Obtains validity of the file.
		/// </summary>
		/// <remarks>
		/// Validity is defined as the presence of a value for <see cref="RootTag"/>.
		/// </remarks>
		public bool Valid
		{
			get { return (RootTag != null); }
		}
	}
}
